﻿
namespace FastFood.DataAccess
{
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Data.Common;
    using System.Linq;
    using EDOSwit.Entity;
    using FastFood.Business.Entities;
    using FastFood.Business.Entities.Tipos;
    using Microsoft.Practices.EnterpriseLibrary.Data;

    /// <summary>
    /// Represents the DataAccess of CocinaData.
    /// </summary>
    public class CocinaData : Singleton<CocinaData>
    {
        #region Attributes

        private readonly Database BaseDatos = DatabaseFactory.CreateDatabase();

        #endregion

        #region Clases privadas

        private class Par
        {
            public short almacenID { get; set; }
            public int loteID { get; set; }

            public Par(short almacenID, int loteID)
            {
                this.almacenID = almacenID;
                this.loteID = loteID;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Selects all records from the Cocina table by a foreign key.
        /// </summary>
        public IList<Cocina> Listar(DateTime? fecha, char estado)
        {
            var entidades = new List<Cocina>();
            var comando = BaseDatos.GetStoredProcCommand("SelectCocinaByFechaAndEstado");

            BaseDatos.AddInParameter(comando, "fecha", DbType.DateTime, fecha);
            BaseDatos.AddInParameter(comando, "estado", DbType.String, estado);

            using (var lector = BaseDatos.ExecuteReader(comando))
            {
                while (lector.Read())
                {
                    var cocina = new Cocina
                    {
                        cocinaID = lector.IsDBNull(lector.GetOrdinal("cocinaID")) ? default(int) : lector.GetInt32(lector.GetOrdinal("cocinaID")),
                        fecha = lector.IsDBNull(lector.GetOrdinal("fecha")) ? default(DateTime) : lector.GetDateTime(lector.GetOrdinal("fecha")),
                        personalID = lector.IsDBNull(lector.GetOrdinal("personalID")) ? default(short) : lector.GetInt16(lector.GetOrdinal("personalID")),
                        loteID = lector.IsDBNull(lector.GetOrdinal("loteID")) ? default(int) : lector.GetInt32(lector.GetOrdinal("loteID")),
                        almacenID = lector.IsDBNull(lector.GetOrdinal("almacenID")) ? default(short) : lector.GetInt16(lector.GetOrdinal("almacenID")),
                        cantidad = lector.IsDBNull(lector.GetOrdinal("cantidad")) ? default(float) : Convert.ToSingle(lector.GetValue(lector.GetOrdinal("cantidad"))),
                        estado = lector.IsDBNull(lector.GetOrdinal("estado")) ? default(char) : lector.GetString(lector.GetOrdinal("estado"))[0],
                        Lotes = LotesData.Instancia.Obtener(lector.IsDBNull(lector.GetOrdinal("loteID")) ? default(int) : lector.GetInt32(lector.GetOrdinal("loteID"))),
                        Almacenes = AlmacenesData.Instancia.Obtener(lector.IsDBNull(lector.GetOrdinal("almacenID")) ? default(short) : lector.GetInt16(lector.GetOrdinal("almacenID"))),
                        NombreInsumo = lector.IsDBNull(lector.GetOrdinal("insumo")) ? default(string) : lector.GetString(lector.GetOrdinal("insumo"))
                    };
                    entidades.Add(cocina);
                }
            }
            comando.Dispose();
            return entidades;
        }

        /// <summary>
        /// Selects all records from the Cocina table by a foreign key.
        /// </summary>
        public Cocina Obtener(int cocinaID)
        {
            var cocina = default(Cocina);
            var comando = BaseDatos.GetStoredProcCommand("SelectCocina");

            BaseDatos.AddInParameter(comando, "cocinaID", DbType.Int32, cocinaID);

            using (var lector = BaseDatos.ExecuteReader(comando))
            {
                while (lector.Read())
                {
                    cocina = new Cocina
                    {
                        cocinaID = lector.IsDBNull(lector.GetOrdinal("cocinaID")) ? default(int) : lector.GetInt32(lector.GetOrdinal("cocinaID")),
                        fecha = lector.IsDBNull(lector.GetOrdinal("fecha")) ? default(DateTime) : lector.GetDateTime(lector.GetOrdinal("fecha")),
                        personalID = lector.IsDBNull(lector.GetOrdinal("personalID")) ? default(short) : lector.GetInt16(lector.GetOrdinal("personalID")),
                        loteID = lector.IsDBNull(lector.GetOrdinal("loteID")) ? default(int) : lector.GetInt32(lector.GetOrdinal("loteID")),
                        almacenID = lector.IsDBNull(lector.GetOrdinal("almacenID")) ? default(short) : lector.GetInt16(lector.GetOrdinal("almacenID")),
                        cantidad = lector.IsDBNull(lector.GetOrdinal("cantidad")) ? default(float) : Convert.ToSingle(lector.GetValue(lector.GetOrdinal("cantidad"))),
                        estado = lector.IsDBNull(lector.GetOrdinal("estado")) ? default(char) : lector.GetString(lector.GetOrdinal("estado"))[0],
                        InsumoID = lector.IsDBNull(lector.GetOrdinal("insumoID")) ? default(short) : lector.GetInt16(lector.GetOrdinal("insumoID"))
                    };
                }
            }
            comando.Dispose();
            return cocina;
        }

        /// <summary>
        /// Saves a record to the Cocina table.
        /// </summary>
        public int Agregar(Cocina cocina)
        {
            var cocinaID = 0;
            var stockAlmacenamientos = new Dictionary<Par, double>();
            var stockLotes = new Dictionary<int, double>();  

            using (DbConnection conexion = BaseDatos.CreateConnection())
            {
                conexion.Open();
                DbTransaction transaccion = conexion.BeginTransaction();

                try
                {
                    var comando = BaseDatos.GetStoredProcCommand("InsertCocina");
                    BaseDatos.AddOutParameter(comando, "cocinaID", DbType.Int32, 4);
                    BaseDatos.AddInParameter(comando, "fecha", DbType.DateTime, cocina.fecha);
                    BaseDatos.AddInParameter(comando, "personalID", DbType.Int16, cocina.personalID);
                    BaseDatos.AddInParameter(comando, "loteID", DbType.Int32, cocina.loteID);
                    BaseDatos.AddInParameter(comando, "almacenID", DbType.Int16, cocina.almacenID);
                    BaseDatos.AddInParameter(comando, "cantidad", DbType.Single, cocina.cantidad);
                    BaseDatos.AddInParameter(comando, "estado", DbType.String, 'R');

                    var resultado = BaseDatos.ExecuteNonQuery(comando, transaccion);
                    if (resultado == 0) throw new Exception("Error al Agregar Cocina.");
                    cocinaID = (int)BaseDatos.GetParameterValue(comando, "cocinaID");

                    AlmacenamientosData.Instancia.ModificarStock(cocina.loteID, cocina.almacenID, cocina.cantidad, transaccion);
                    LotesData.Instancia.ModificarStock(cocina.loteID, cocina.cantidad, transaccion);
                    InsumosData.Instancia.ModificarStock(cocina.InsumoID, cocina.cantidad, transaccion);

                    cocina.Movimiento.operacionID = cocinaID;
                    MovimientosData.Instancia.AgregarConFecha(cocina.Movimiento, transaccion);

                    foreach (var insumo in cocina.Insumos)
                    {
                        if (RegistrarSalidaInsumos(stockAlmacenamientos, stockLotes, insumo.insumoID, insumo.cantidad, cocinaID, cocina, transaccion) > 0)
                        {
                            throw new Exception("No hay suficiente " + insumo.nombreInsumo + " para realizar la preparación.");
                        }
                    }

                    foreach (var almacen in stockAlmacenamientos)
                    {
                        AlmacenamientosData.Instancia.ModificarStock(almacen.Key.loteID, almacen.Key.almacenID, almacen.Value, transaccion);
                    }

                    foreach (var lote in stockLotes)
                    {
                        LotesData.Instancia.ModificarStock(lote.Key, lote.Value, transaccion);
                    }

                    foreach (var insumo in cocina.Insumos)
                    {
                        InsumosData.Instancia.ModificarStock(insumo.insumoID, -insumo.cantidad, transaccion);
                    }

                    transaccion.Commit();
                }
                catch (Exception ex)
                {
                    transaccion.Rollback();
                    throw new Exception(ex.Message);
                }
                conexion.Close();
            }
            return cocinaID;
        }

        /// <summary>
        /// Updates a record in the Cocina table.
        /// </summary>
        public bool Modificar(Cocina cocina)
        {
            var stockAlmacenamientos = new Dictionary<Par, double>();
            var stockLotes = new Dictionary<int, double>();

            using (DbConnection conexion = BaseDatos.CreateConnection())
            {
                conexion.Open();
                DbTransaction transaccion = conexion.BeginTransaction();

                try
                {
                    var cocinaAntigua = Obtener(cocina.cocinaID);

                    var comando = BaseDatos.GetStoredProcCommand("UpdateCocina");
                    BaseDatos.AddInParameter(comando, "cocinaID", DbType.Int32, cocina.cocinaID);
                    BaseDatos.AddInParameter(comando, "fecha", DbType.DateTime, cocina.fecha);
                    BaseDatos.AddInParameter(comando, "personalID", DbType.Int16, cocina.personalID);
                    BaseDatos.AddInParameter(comando, "loteID", DbType.Int32, cocina.loteID);
                    BaseDatos.AddInParameter(comando, "almacenID", DbType.Int16, cocina.almacenID);
                    BaseDatos.AddInParameter(comando, "cantidad", DbType.Single, cocina.cantidad);
                    BaseDatos.AddInParameter(comando, "estado", DbType.String, 'R');

                    var resultado = BaseDatos.ExecuteNonQuery(comando, transaccion);
                    if (resultado == 0) throw new Exception("Error al Modificar Cocina.");

                    #region Devolucion de los insumos utilizados

                    var cantidadMovimientos = cocina.Insumos.Count + 1;
                    var movimientos = MovimientosData.Instancia.ListarPorOperacionID(cocina.cocinaID);
                    movimientos = movimientos.Where(p => p.fecha == cocinaAntigua.fecha).ToList();
                    movimientos = movimientos.OrderByDescending(p => p.movimientoID).ToList();
                    movimientos = movimientos.Take(cantidadMovimientos).ToList();
                    movimientos = movimientos.OrderBy(p => p.movimientoID).ToList();

                    var movimientoDevolucion = new Movimientos
                            {
                                operacionID = cocina.cocinaID,
                                fecha = cocina.fecha,
                                motivo = (byte)MotivoMovimiento.Cocina,
                                personalID = cocina.personalID
                            };

                    foreach (var movimiento in movimientos)
                    {
                        movimiento.cantidad = -1 * movimiento.cantidad;
                    }

                    foreach (var movimiento in movimientos)
                    {
                        AlmacenamientosData.Instancia.ModificarStock(movimiento.loteID, movimiento.almacenID, movimiento.cantidad, transaccion);
                    }

                    foreach (var movimiento in movimientos)
                    {
                        LotesData.Instancia.ModificarStock(movimiento.loteID, movimiento.cantidad, transaccion);
                    }

                    foreach (var movimiento in movimientos)
                    {
                        double stockInsumo = 0;
                        var comandoInsumo = BaseDatos.GetStoredProcCommand("SelectInsumos");

                        BaseDatos.AddInParameter(comandoInsumo, "insumoID", DbType.Int16, movimiento.insumoID);

                        using (var lector = BaseDatos.ExecuteReader(comandoInsumo, transaccion))
                        {
                            if (lector.Read())
                            {
                                stockInsumo = lector.GetDouble(lector.GetOrdinal("stock"));
                            }
                        }
                        comandoInsumo.Dispose();

                        movimientoDevolucion.loteID = movimiento.loteID;
                        movimientoDevolucion.almacenID = movimiento.almacenID;
                        movimientoDevolucion.cantidad = movimiento.cantidad;
                        movimientoDevolucion.saldo = stockInsumo + movimiento.cantidad;

                        InsumosData.Instancia.ModificarStock(movimiento.insumoID, movimiento.cantidad, transaccion);
                        MovimientosData.Instancia.AgregarConFecha(movimientoDevolucion, transaccion);
                    }

                    #endregion

                    #region Registro de los insumos utilizados

                    double stock = 0;
                    var comandoSemiTerminado = BaseDatos.GetStoredProcCommand("SelectInsumos");

                    BaseDatos.AddInParameter(comandoSemiTerminado, "insumoID", DbType.Int16, cocina.InsumoID);
                    using (var lector = BaseDatos.ExecuteReader(comandoSemiTerminado, transaccion))
                    {
                        if (lector.Read())
                        {
                            stock = lector.GetDouble(lector.GetOrdinal("stock"));
                        }
                    }
                    comandoSemiTerminado.Dispose();

                    cocina.Movimiento.saldo = stock + cocina.cantidad;
                    cocina.Movimiento.operacionID = cocina.cocinaID;
                    AlmacenamientosData.Instancia.ModificarStock(cocina.loteID, cocina.almacenID, cocina.cantidad, transaccion);
                    LotesData.Instancia.ModificarStock(cocina.loteID, cocina.cantidad, transaccion);
                    InsumosData.Instancia.ModificarStock(cocina.InsumoID, cocina.cantidad, transaccion);
                    MovimientosData.Instancia.AgregarConFecha(cocina.Movimiento, transaccion);

                    foreach (var item in cocina.Insumos)
                    {
                        if (RegistrarSalidaInsumos(stockAlmacenamientos, stockLotes, item.insumoID, item.cantidad, cocina.cocinaID, cocina, transaccion) > 0)
                        {
                            throw new Exception("No hay suficiente " + item.nombreInsumo + " para realizar la preparación.");
                        }
                    }

                    foreach (var almacen in stockAlmacenamientos)
                    {
                        AlmacenamientosData.Instancia.ModificarStock(almacen.Key.loteID, almacen.Key.almacenID, almacen.Value, transaccion);
                    }

                    foreach (var lote in stockLotes)
                    {
                        LotesData.Instancia.ModificarStock(lote.Key, lote.Value, transaccion);
                    }

                    foreach (var item in cocina.Insumos)
                    {
                        InsumosData.Instancia.ModificarStock(item.insumoID, -item.cantidad, transaccion);
                    }

                    #endregion

                    transaccion.Commit();
                }
                catch (Exception ex)
                {
                    transaccion.Rollback();
                    throw new Exception(ex.Message);
                }
                conexion.Close();
            }
            return true;
        }

        /// <summary>
        /// Deletes a record from the Cocina table by its primary key.
        /// </summary>
        public bool Eliminar(Cocina cocina)
        {
            using (DbConnection conexion = BaseDatos.CreateConnection())
            {
                conexion.Open();
                DbTransaction transaccion = conexion.BeginTransaction();

                try
                {
                    var cocinaAntigua = Obtener(cocina.cocinaID);

                    var comando = BaseDatos.GetStoredProcCommand("DeleteCocina");
                    BaseDatos.AddInParameter(comando, "cocinaID", DbType.Int32, cocina.cocinaID);

                    var resultado = BaseDatos.ExecuteNonQuery(comando, transaccion);
                    if (resultado == 0) throw new Exception("Error al Eliminar Cocina.");

                    #region Devolucion de los insumos utilizados

                    var cantidadMovimientos = cocina.Insumos.Count + 1;
                    var movimientos = MovimientosData.Instancia.ListarPorOperacionID(cocina.cocinaID);
                    movimientos = movimientos.Where(p => p.fecha == cocinaAntigua.fecha).ToList();
                    movimientos = movimientos.OrderByDescending(p => p.movimientoID).ToList();
                    movimientos = movimientos.Take(cantidadMovimientos).ToList();
                    movimientos = movimientos.OrderBy(p => p.movimientoID).ToList();

                    var movimientoDevolucion = new Movimientos
                    {
                        operacionID = cocina.cocinaID,
                        fecha = cocina.fecha,
                        motivo = (byte)MotivoMovimiento.Cocina,
                        personalID = cocina.personalID
                    };

                    foreach (var movimiento in movimientos)
                    {
                        movimiento.cantidad = -1 * movimiento.cantidad;
                    }

                    foreach (var movimiento in movimientos)
                    {
                        AlmacenamientosData.Instancia.ModificarStock(movimiento.loteID, movimiento.almacenID, movimiento.cantidad, transaccion);
                    }

                    foreach (var movimiento in movimientos)
                    {
                        LotesData.Instancia.ModificarStock(movimiento.loteID, movimiento.cantidad, transaccion);
                    }

                    foreach (var movimiento in movimientos)
                    {
                        double stockInsumo = 0;
                        var comandoInsumo = BaseDatos.GetStoredProcCommand("SelectInsumos");

                        BaseDatos.AddInParameter(comandoInsumo, "insumoID", DbType.Int16, movimiento.insumoID);

                        using (var lector = BaseDatos.ExecuteReader(comandoInsumo, transaccion))
                        {
                            if (lector.Read())
                            {
                                stockInsumo = lector.GetDouble(lector.GetOrdinal("stock"));
                            }
                        }
                        comandoInsumo.Dispose();

                        movimientoDevolucion.loteID = movimiento.loteID;
                        movimientoDevolucion.almacenID = movimiento.almacenID;
                        movimientoDevolucion.cantidad = movimiento.cantidad;
                        movimientoDevolucion.saldo = stockInsumo + movimiento.cantidad;

                        InsumosData.Instancia.ModificarStock(movimiento.insumoID, movimiento.cantidad, transaccion);
                        MovimientosData.Instancia.AgregarConFecha(movimientoDevolucion, transaccion);
                    }

                    #endregion

                    transaccion.Commit();
                }
                catch (Exception ex)
                {
                    transaccion.Rollback();
                    throw new Exception(ex.Message);
                }
                conexion.Close();
            }
            return true;
        }

        private double RegistrarSalidaInsumos(Dictionary<Par, double> stockAlmacenamientos, Dictionary<int, double> stockLotes, 
            short insumoID, double cantidad, int cocinaID, Cocina cocina, DbTransaction transaccion)
        {
            var movimiento = new Movimientos
                            {
                                operacionID = cocinaID,
                                fecha = cocina.fecha,
                                personalID = cocina.personalID,
                                motivo = (byte)MotivoMovimiento.Cocina
                            };

            var lotes = new List<Lotes>();
            var comando = BaseDatos.GetStoredProcCommand("SelectLotesVendiblesByInsumoID");
            BaseDatos.AddInParameter(comando, "insumoID", DbType.Int16, insumoID);

            using (var lector = BaseDatos.ExecuteReader(comando, transaccion))
            {
                while (lector.Read())
                {
                    lotes.Add(new Lotes
                    {
                        loteID = lector.IsDBNull(lector.GetOrdinal("loteID")) ? default(int) : lector.GetInt32(lector.GetOrdinal("loteID")),
                        insumoID = lector.IsDBNull(lector.GetOrdinal("insumoID")) ? default(short) : lector.GetInt16(lector.GetOrdinal("insumoID")),
                        stock = lector.IsDBNull(lector.GetOrdinal("stock")) ? default(double) : lector.GetDouble(lector.GetOrdinal("stock")),
                    });
                }
            }
            comando.Dispose();

            foreach (var lote in lotes)
            {
                double subTotal = 0;
                movimiento.loteID = lote.loteID;

                var almacenamientos = new List<Almacenamientos>();
                var comandoAlmacen = BaseDatos.GetStoredProcCommand("SelectAlmacenamientosVendiblesByLoteID");

                BaseDatos.AddInParameter(comandoAlmacen, "loteID", DbType.Int32, lote.loteID);

                using (var lector = BaseDatos.ExecuteReader(comandoAlmacen, transaccion))
                {
                    while (lector.Read())
                    {
                        almacenamientos.Add(new Almacenamientos
                        {
                            loteID = lector.IsDBNull(lector.GetOrdinal("loteID")) ? default(int) : lector.GetInt32(lector.GetOrdinal("loteID")),
                            almacenID = lector.IsDBNull(lector.GetOrdinal("almacenID")) ? default(short) : lector.GetInt16(lector.GetOrdinal("almacenID")),
                            stock = lector.IsDBNull(lector.GetOrdinal("stock")) ? default(double) : lector.GetDouble(lector.GetOrdinal("stock"))
                        });
                    }
                }
                comandoAlmacen.Dispose();

                foreach (var almacenamiento in almacenamientos)
                {
                    movimiento.almacenID = almacenamiento.almacenID;

                    if (almacenamiento.stock < cantidad)
                    {  
                        stockAlmacenamientos.Add(new Par(almacenamiento.almacenID, almacenamiento.loteID), -almacenamiento.stock);
                        movimiento.cantidad = -1 * almacenamiento.stock;
                        movimiento.saldo = 0;
                        MovimientosData.Instancia.AgregarConFecha(movimiento, transaccion);
                        subTotal += almacenamiento.stock;
                        cantidad -= almacenamiento.stock;
                    }
                    else
                    {
                        stockAlmacenamientos.Add(new Par(almacenamiento.almacenID, almacenamiento.loteID), -cantidad);
                        movimiento.cantidad = -1 * cantidad;
                        movimiento.saldo = almacenamiento.stock - cantidad;
                        MovimientosData.Instancia.AgregarConFecha(movimiento, transaccion);
                        subTotal += cantidad;
                        cantidad = 0;
                        break;
                    }
                }
                stockLotes.Add(lote.loteID, -subTotal);
                if (cantidad == 0) break;
            }
            return cantidad;
        }

        #endregion
    }
}
